﻿// SimpleZip.cs
//
// Copyright 2005 John Reilly
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//
// Linking this library statically or dynamically with other modules is
// making a combined work based on this library.  Thus, the terms and
// conditions of the GNU General Public License cover the whole
// combination.
// 
// As a special exception, the copyright holders of this library give you
// permission to link this library with independent modules to produce an
// executable, regardless of the license terms of these independent
// modules, and to copy and distribute the resulting executable under
// terms of your choice, provided that you also meet, for each linked
// independent module, the terms and conditions of the license of that
// module.  An independent module is a module which is not derived from
// or based on this library.  If you modify this library, you may extend
// this exception to your version of the library, but you are not
// obligated to do so.  If you do not wish to do so, delete this
// exception statement from your version.


using System;
using System.IO;
using ICSharpCode.SharpZipLib.Core;

namespace ICSharpCode.SharpZipLib.Zip
{
    /// <summary>
    /// FastZipEvents supports all events applicable to <see cref="FastZip">FastZip</see> operations.
    /// </summary>
    public class FastZipEvents
    {
        /// <summary>
        /// Delegate to invoke when processing directories.
        /// </summary>
        public ProcessDirectoryDelegate ProcessDirectory;

        /// <summary>
        /// Delegate to invoke when processing files.
        /// </summary>
        public ProcessFileDelegate ProcessFile;

        /// <summary>
        /// Delegate to invoke when processing directory failures.
        /// </summary>
        public DirectoryFailureDelegate DirectoryFailure;

        /// <summary>
        /// Delegate to invoke when processing file failures.
        /// </summary>
        public FileFailureDelegate FileFailure;

        /// <summary>
        /// Raise the directory failure event.
        /// </summary>
        /// <param name="directory">The directory.</param>
        /// <param name="e">The exception for this event.</param>
        public void OnDirectoryFailure(string directory, Exception e)
        {
            if (DirectoryFailure != null)
            {
                ScanFailureEventArgs args = new ScanFailureEventArgs(directory, e);
                DirectoryFailure(this, args);
            }
        }

        /// <summary>
        /// Raises the file failure event.
        /// </summary>
        /// <param name="file">The file for this event.</param>
        /// <param name="e">The exception for this event.</param>
        public void OnFileFailure(string file, Exception e)
        {
            if (FileFailure != null)
            {
                ScanFailureEventArgs args = new ScanFailureEventArgs(file, e);
                FileFailure(this, args);
            }
        }

        /// <summary>
        /// Raises the ProcessFileEvent.
        /// </summary>
        /// <param name="file">The file for this event.</param>
        public void OnProcessFile(string file)
        {
            if (ProcessFile != null)
            {
                ScanEventArgs args = new ScanEventArgs(file);
                ProcessFile(this, args);
            }
        }

        /// <summary>
        /// Raises the ProcessDirectoryEvent.
        /// </summary>
        /// <param name="directory">The directory for this event.</param>
        /// <param name="hasMatchingFiles">Flag indicating if directory has matching files as determined by the current filter.</param>
        public void OnProcessDirectory(string directory, bool hasMatchingFiles)
        {
            if (ProcessDirectory != null)
            {
                DirectoryEventArgs args = new DirectoryEventArgs(directory, hasMatchingFiles);
                ProcessDirectory(this, args);
            }
        }

    }

    /// <summary>
    /// FastZip provides facilities for creating and extracting zip files.
    /// Only relative paths are supported.
    /// </summary>
    public class FastZip
    {
        /// <summary>
        /// Initialize a default instance of FastZip.
        /// </summary>
        public FastZip()
        {
            this.events = null;
        }

        /// <summary>
        /// Initialise a new instance of <see cref="FastZip"/>
        /// </summary>
        /// <param name="events"></param>
        public FastZip(FastZipEvents events)
        {
            this.events = events;
        }

        /// <summary>
        /// Defines the desired handling when overwriting files.
        /// </summary>
        public enum Overwrite
        {
            /// <summary>
            /// Prompt the user to confirm overwriting
            /// </summary>
            Prompt,
            /// <summary>
            /// Never overwrite files.
            /// </summary>
            Never,
            /// <summary>
            /// Always overwrite files.
            /// </summary>
            Always
        }

        /// <summary>
        /// Get/set a value indicating wether empty directories should be created.
        /// </summary>
        public bool CreateEmptyDirectories
        {
            get { return createEmptyDirectories; }
            set { createEmptyDirectories = value; }
        }

        /// <summary>
        /// Delegate called when confirming overwriting of files.
        /// </summary>
        public delegate bool ConfirmOverwriteDelegate(string fileName);

        /// <summary>
        /// Create a zip file.
        /// </summary>
        /// <param name="zipFileName">The name of the zip file to create.</param>
        /// <param name="sourceDirectory">The directory to source files from.</param>
        /// <param name="recurse">True to recurse directories, false for no recursion.</param>
        /// <param name="fileFilter">The file filter to apply.</param>
        /// <param name="directoryFilter">The directory filter to apply.</param>
        public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter)
        {
            NameTransform = new ZipNameTransform(true, sourceDirectory);
            this.sourceDirectory = sourceDirectory;

            outputStream = new ZipOutputStream(File.Create(zipFileName));
            try
            {
                FileSystemScanner scanner = new FileSystemScanner(fileFilter, directoryFilter);
                scanner.ProcessFile += new ProcessFileDelegate(ProcessFile);
                if (this.CreateEmptyDirectories)
                {
                    scanner.ProcessDirectory += new ProcessDirectoryDelegate(ProcessDirectory);
                }
                scanner.Scan(sourceDirectory, recurse);
            }
            finally
            {
                outputStream.Close();
            }
        }

        /// <summary>
        /// Create a zip file/archive.
        /// </summary>
        /// <param name="zipFileName">The name of the zip file to create.</param>
        /// <param name="sourceDirectory">The directory to obtain files and directories from.</param>
        /// <param name="recurse">True to recurse directories, false for no recursion.</param>
        /// <param name="fileFilter">The file filter to apply.</param>
        public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, string fileFilter)
        {
            CreateZip(zipFileName, sourceDirectory, recurse, fileFilter, null);
        }

        /// <summary>
        /// Extract the contents of a zip file.
        /// </summary>
        /// <param name="zipFileName">The zip file to extract from.</param>
        /// <param name="targetDirectory">The directory to save extracted information in.</param>
        /// <param name="fileFilter">A filter to apply to files.</param>
        public void ExtractZip(string zipFileName, string targetDirectory, string fileFilter)
        {
            ExtractZip(zipFileName, targetDirectory, Overwrite.Always, null, fileFilter, null);
        }

        /// <summary>
        /// Exatract the contents of a zip file.
        /// </summary>
        /// <param name="zipFileName">The zip file to extract from.</param>
        /// <param name="targetDirectory">The directory to save extracted information in.</param>
        /// <param name="overwrite">The style of <see cref="Overwrite">overwriting</see> to apply.</param>
        /// <param name="confirmDelegate">A delegate to invoke when confirming overwriting.</param>
        /// <param name="fileFilter">A filter to apply to files.</param>
        /// <param name="directoryFilter">A filter to apply to directories.</param>
        public void ExtractZip(string zipFileName, string targetDirectory,
                               Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate,
                               string fileFilter, string directoryFilter)
        {
            if ((overwrite == Overwrite.Prompt) && (confirmDelegate == null))
            {
                throw new ArgumentNullException("confirmDelegate");
            }
            this.overwrite = overwrite;
            this.confirmDelegate = confirmDelegate;
            this.targetDirectory = targetDirectory;
            this.fileFilter = new NameFilter(fileFilter);
            this.directoryFilter = new NameFilter(directoryFilter);

            inputStream = new ZipInputStream(File.OpenRead(zipFileName));

            try
            {

                if (password != null)
                {
                    inputStream.Password = password;
                }

                ZipEntry entry;
                while ((entry = inputStream.GetNextEntry()) != null)
                {
                    if (this.directoryFilter.IsMatch(Path.GetDirectoryName(entry.Name)) && this.fileFilter.IsMatch(entry.Name))
                    {
                        ExtractEntry(entry);
                    }
                }
            }
            finally
            {
                inputStream.Close();
            }
        }

        void ProcessDirectory(object sender, DirectoryEventArgs e)
        {
            if (!e.HasMatchingFiles && createEmptyDirectories)
            {
                if (events != null)
                {
                    events.OnProcessDirectory(e.Name, e.HasMatchingFiles);
                }

                if (e.Name != sourceDirectory)
                {
                    string cleanedName = nameTransform.TransformDirectory(e.Name);
                    ZipEntry entry = new ZipEntry(cleanedName);
                    outputStream.PutNextEntry(entry);
                }
            }
        }

        void ProcessFile(object sender, ScanEventArgs e)
        {
            if (events != null)
            {
                events.OnProcessFile(e.Name);
            }
            string cleanedName = nameTransform.TransformFile(e.Name);
            ZipEntry entry = new ZipEntry(cleanedName);
            outputStream.PutNextEntry(entry);
            AddFileContents(e.Name);
        }

        void AddFileContents(string name)
        {
            if (buffer == null)
            {
                buffer = new byte[4096];
            }

            FileStream stream = File.OpenRead(name);
            try
            {
                int length;
                do
                {
                    length = stream.Read(buffer, 0, buffer.Length);
                    outputStream.Write(buffer, 0, length);
                } while (length > 0);
            }
            finally
            {
                stream.Close();
            }
        }

        void ExtractFileEntry(ZipEntry entry, string targetName)
        {
            bool proceed = true;
            if ((overwrite == Overwrite.Prompt) && (confirmDelegate != null))
            {
                if (File.Exists(targetName) == true)
                {
                    proceed = confirmDelegate(targetName);
                }
            }

            if (proceed)
            {

                if (events != null)
                {
                    events.OnProcessFile(entry.Name);
                }

                FileStream streamWriter = File.Create(targetName);

                try
                {
                    if (buffer == null)
                    {
                        buffer = new byte[4096];
                    }

                    int size;

                    do
                    {
                        size = inputStream.Read(buffer, 0, buffer.Length);
                        streamWriter.Write(buffer, 0, size);
                    } while (size > 0);
                }
                finally
                {
                    streamWriter.Close();
                }

                if (restoreDateTime)
                {
                    File.SetLastWriteTime(targetName, entry.DateTime);
                }
            }
        }

        bool NameIsValid(string name)
        {
            return name != null && name.Length > 0 && name.IndexOfAny(Path.InvalidPathChars) < 0;
        }

        void ExtractEntry(ZipEntry entry)
        {
            bool doExtraction = NameIsValid(entry.Name);

            string dirName = null;
            string targetName = null;

            if (doExtraction)
            {
                string entryFileName;
                if (Path.IsPathRooted(entry.Name))
                {
                    string workName = Path.GetPathRoot(entry.Name);
                    workName = entry.Name.Substring(workName.Length);
                    entryFileName = Path.Combine(Path.GetDirectoryName(workName), Path.GetFileName(entry.Name));
                }
                else
                {
                    entryFileName = entry.Name;
                }

                targetName = Path.Combine(targetDirectory, entryFileName);
                dirName = Path.GetDirectoryName(Path.GetFullPath(targetName));

                doExtraction = doExtraction && (entryFileName.Length > 0);
            }

            if (doExtraction && !Directory.Exists(dirName))
            {
                if (!entry.IsDirectory || this.CreateEmptyDirectories)
                {
                    try
                    {
                        Directory.CreateDirectory(dirName);
                    }
                    catch
                    {
                        doExtraction = false;
                    }
                }
            }

            if (doExtraction && entry.IsFile)
            {
                ExtractFileEntry(entry, targetName);
            }
        }

        /// <summary>
        /// Get or set the <see cref="ZipNameTransform"> active when creating Zip files.</see>
        /// </summary>
        public ZipNameTransform NameTransform
        {
            get { return nameTransform; }
            set
            {
                if (value == null)
                {
                    nameTransform = new ZipNameTransform();
                }
                else
                {
                    nameTransform = value;
                }
            }
        }

        #region Instance Fields
        byte[] buffer;
        ZipOutputStream outputStream;
        ZipInputStream inputStream;
        string password = null;
        string targetDirectory;
        string sourceDirectory;
        NameFilter fileFilter;
        NameFilter directoryFilter;
        Overwrite overwrite;
        ConfirmOverwriteDelegate confirmDelegate;
        bool restoreDateTime = false;
        bool createEmptyDirectories = false;
        FastZipEvents events;
        ZipNameTransform nameTransform;
        #endregion
    }
}